<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <title>贪吃蛇游戏的设计与实现</title>
    <style>
        #gameOverDialog {
            position: fixed;
            top: 50%;
            left: 50%;
            transform: translate(-50%, -50%);
            background-color: white;
            padding: 20px;
            border: 1px solid black;
            z-index: 1000;
        }

    </style>
</head>
<body>
    <div id="container">
        <h3>贪吃蛇</h3>
        <hr>
        <!-- 状态信息栏 -->
        <div id="status">
            <!-- 历史最高分 -->
            <div class="box">
                历史最高分：<span id="bestScore">0</span>
            </div>
            <!-- 当前分数 -->
            <div class="box">
                当前分数：<span id="currentScore">0</span>
            </div>
        </div>
        <!-- 设置游戏画布 -->
        <canvas id="myCanvas" width="400" height="400" style="border:1px solid"></canvas>
        <div>
            <button onclick="window.location.reload()">
                重新开始
            </button>
        </div>
        <div id="gameOverDialog" style="display:none;">
            <p>游戏失败!当前得分：<span id="gameOverScore">0</span></p>
            <button onclick="restartGame()">重新开始</button>
        </div>
    </div>
    <script>
        // 游戏参数设置
        // 游戏界面刷新的间隔时间（数字越大，蛇的速度越慢）
        let time = 200;
        // 定义了一个变量 time，表示游戏界面刷新的时间间隔，初始值为 200 毫秒。

        let speedInterval = time;   //定义一个变量来存储当前的游戏速度间隔时间，初始值为time。

        // 蛇身长
        let t = 3;
        // 定义变量 t，表示蛇的初始长度为 3。

        // 记录蛇运行轨迹，用数组记录每一个坐标点
        let snakeMap = [];
        // 创建一个数组 snakeMap，用于记录蛇的运行轨迹，每个元素是一个包含 x 和 y 坐标的对象。

        // 蛇身单元大小
        let w = 10;
        // 定义变量 w，表示蛇身的单元大小为 10。

        // 方向代码：左37，上38，右39，下40
        let direction = 37;
        // 定义变量 direction，表示蛇的初始方向为左（方向代码为 37）。

        // 蛇的初始坐标
        let x = 0;
        let y = 0;
        // 定义变量 x 和 y，表示蛇的初始坐标为 (0, 0)。

        // 食物的初始化坐标
        let foodX = 0;
        let foodY = 0;
        // 定义变量 foodX 和 foodY，表示食物的初始坐标为 (0, 0)。

        // 当前得分
        let score = 0;
        // 定义变量 score，表示当前得分，初始值为 0。

        // 历史最高分纪录
        let bestScore = parseInt(localStorage.getItem('bestScore')) || 0;
        // 从本地存储中读取历史最高分，如果没有则初始化为 0。（游戏失败再更新）

        // 画布的宽和高
        let width = 400;
        let height = 400;
        // 定义变量 width 和 height，表示画布的宽度和高度为 400。

        // 根据 id 找到指定的画布
        const c = document.getElementById('myCanvas');
        // 通过文档对象的 getElementById 方法获取 id 为 myCanvas 的元素，即画布。

        // 创建 2D 的 context 对象
        const ctx = c.getContext('2d');
        // 在获取的画布上创建一个 2D 的绘图上下文对象 ctx。

        // 显示历史最高分记录
        showBestScore();
        // 调用函数 showBestScore 来显示历史最高分。

        // 开始游戏
        GameStart();
        // 调用函数 GameStart 开始游戏。

        // 启动游戏（主程序）
        function GameStart() {
            // 调用 drawFood()函数，在随机位置绘制第一个食物
            drawFood();

            // 随机生成贪吃蛇的蛇头坐标
            /*
            x = Math.floor(Math.random() * width / w) * w;
            y = Math.floor(Math.random() * height / w) * w;
            */
            // 生成蛇头的初始坐标，确保坐标是蛇身单元大小 w 的倍数。
            x = (width + 1500) / w;
            y = (height + 1500) / w;
            

            // 随机生成蛇的初始前进方向，方向代码为 37（左）、38（上）、39（右）、40（下）中的一个。
            direction = 37 + Math.floor(Math.random() * 4);
            
            // 保存定时器的返回值
            window.intervalId = setInterval(gameRefresh, time);
            // 使用 setInterval 函数创建一个定时器，每隔 time 毫秒调用一次 gameRefresh 函数，并将定时器的返回值保存在 window.intervalId 中。
        }


        // 游戏画面刷新函数
        function gameRefresh() {
            // 将当前坐标数据添加到贪吃蛇的运动轨迹坐标数组中
            snakeMap.push({ 'x': x, 'y': y });
            // 将当前蛇头的坐标添加到 snakeMap 数组中，记录蛇的运动轨迹。

            // 绘制贪吃蛇
            drawSnake();
            // 调用 drawSnake 函数绘制贪吃蛇。

            // 根据方向移动蛇头的下一个位置
            switch (direction) {
                case 37:
                    x -= w;
                    break;
                case 38:
                    y -= w;
                    break;
                case 39:
                    x += w;
                    break;
                case 40:
                    y += w;
                    break;
            }
            // 根据当前的方向（direction）变量，更新蛇头的坐标 x 和 y。

            // 碰撞检测，返回值 0 表示没有撞到障碍物
            const code = detectCollision();
            // 调用 detectCollision 函数进行碰撞检测，并将返回值保存在 code 变量中。

            // 如果返回值不为 0，表示游戏失败
            if (code!== 0) {
                if (score > bestScore) {
                    bestScore = score;
                    localStorage.setItem('bestScore', bestScore);
                }
                showGameOverDialog();
                return;
            }
            // 如果碰撞检测的返回值不为 0，表示蛇撞到了墙壁或自身，此时如果当前得分大于历史最高分，则更新历史最高分，并调用 showGameOverDialog 函数显示游戏失败对话框，然后返回，停止游戏刷新。

            // 吃到食物判定
            if (foodX === x && foodY === y) {
                // 吃到一次食物加 10 分
                score += 10;
                // 更新状态栏中的当前分数
                const currentScore = document.getElementById('currentScore');
                currentScore.innerHTML = score;
                // 在随机位置绘制下一个食物
                drawFood();
                // 蛇身长度加 1
                t++;

                speedInterval = Math.max(speedInterval - 10, 50); // 确保速度不会太快，这里设置最小速度间隔为 50
                // 清除之前的定时器
                clearInterval(window.intervalId);
                // 根据新的速度间隔重新设置定时器
                window.intervalId = setInterval(gameRefresh, speedInterval);
            }
            // 如果蛇头的坐标与食物的坐标相同，表示蛇吃到了食物，此时增加当前得分 score，更新页面上显示的当前分数，调用 drawFood 函数绘制新的食物，并增加蛇身长度 t。
        }

        // 绘制贪吃蛇函数
        function drawSnake() {
            // 设置蛇身内部填充颜色
            ctx.fillStyle = 'lightblue';
            // 设置绘制蛇身的填充颜色为浅蓝色。

            // 绘制最新位置的蛇身矩形
            ctx.fillRect(x, y, w, w);
            // 在画布上以当前蛇头的坐标 (x, y) 和蛇身单元大小 w 绘制一个矩形，表示蛇身。

            // 数组只保留蛇身长度的数据，如果蛇前进了则删除最旧的坐标数据
            if (snakeMap.length > t) {
                // 删除数组第一项，即蛇的尾部最后一个位置的坐标记录，并且返回
                const lastBox = snakeMap.shift();
                // 画布清除蛇的尾部最后一个位置，从而实现移动效果
                ctx.clearRect(lastBox['x'], lastBox['y'], w, w);
            }
            // 如果蛇的长度超过了当前的蛇身长度 t，则删除 snakeMap 数组的第一个元素（蛇的尾部坐标），并在画布上清除该位置的矩形，实现蛇的移动效果。
        }

        // 绘制食物函数
        function drawFood() {
            let isOnSnake = true;
            while (isOnSnake)
             {
                foodX = Math.floor(Math.random() * width / w) * w;
                foodY = Math.floor(Math.random() * height / w) * w;

                //保证食物不会生成在蛇身
                isOnSnake = false;
                for (let i = 0; i < snakeMap.length; i++)
                {
                    if (snakeMap[i].x === foodX && snakeMap[i].y === foodY)
                    {
                        isOnSnake = true;
                        break;
                    }
                }
            }
            ctx.fillStyle = '#FF0000';
            ctx.fillRect(foodX, foodY, w, w);
        }

        // 改变蛇方向的按键监听
        document.onkeydown = function (e) {
            // 根据按键更新前进方向 code：左 37，上 38，右 39，下 40
            if (e.keyCode === 37 || e.keyCode === 38 || e.keyCode === 39 || e.keyCode === 40) {
                direction = e.keyCode;
            }
        };
        // 监听文档的 keydown 事件，当按下方向键（左 37、上 38、右 39、下 40）时，更新蛇的前进方向 direction。

        // 碰撞检测函数
        function detectCollision() {
            // 蛇头碰撞到了四周的墙壁，则游戏失败
            if (x > width || y > height || x < 0 || y < 0) {
                return 1;
            }
            // 检查蛇头的坐标是否超出画布范围，如果是，则返回 1 表示撞到墙壁。

            // 蛇头碰撞到了蛇身，则游戏失败
            for (let i = 0; i < snakeMap.length; i++) {
                if (snakeMap[i].x === x && snakeMap[i].y === y) {
                    return 2;
                }
            }
            // 遍历蛇的运动轨迹数组 snakeMap，如果蛇头的坐标与数组中的某个元素（蛇身的一部分）相同，则返回 2 表示撞到蛇身。

            return 0;
            // 如果没有碰撞到墙壁或蛇身，则返回 0 表示没有撞到障碍物。
        }

        function showBestScore() {
            // 将历史最高分更新到状态栏中
            const best = document.getElementById('bestScore');
            best.innerHTML = bestScore;
        }
        // 获取 id 为 bestScore 的元素，将历史最高分 bestScore 显示在该元素中。

        function showGameOverDialog() {
            const dialog = document.getElementById('gameOverDialog');
            const scoreSpan = document.getElementById('gameOverScore');
            scoreSpan.textContent = score;
            dialog.style.display = 'block';
        }
        // 获取 id 为 gameOverDialog 的元素和其中的 id 为 gameOverScore 的元素，将当前得分显示在 gameOverScore 元素中，并显示游戏失败对话框。

        function restartGame() {
            const dialog = document.getElementById('gameOverDialog');
            dialog.style.display = 'none';
            // 隐藏游戏失败对话框。

            // // 将当前得分重置为 0，清空蛇的运动轨迹数组，重置蛇身长度为 3
            score = 0;
            snakeMap = [];
            t = 3;

            // 将速度恢复到初始值
            time = 200;

            // 清除画布上之前的食物
            ctx.clearRect(0, 0, width, height);

            // 重新生成食物
            drawFood();

            // 更新页面上显示的当前分数为 0。
            const currentScore = document.getElementById('currentScore');
            currentScore.innerHTML = score;

            // 更新历史最高分显示
            showBestScore();

            // 清除之前的定时器
            clearInterval(window.intervalId);

            // 重新开始游戏。
            GameStart();
            
        }
    </script>
</body>
</html>